/*
 * Copyright (c) 2011, Yawning <yawninglol at gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   * Neither the name of the Mew Developers nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package yawning.mew.util;

import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

public class URLFetcher {
	private static URLFetcher sInstance;
	private static SimpleDateFormat sDateFormater = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
	private static long CACHE_TTL = (24 * 60 * 60 * 1000);
	private static final int CONNECT_TIMEOUT = (10 * 1000);
	private static final int READ_TIMEOUT = (10 * 1000);
	private static final int BUFFER_SZ = 8192;

	private class CacheEntry {
		long mBodyUpdatedAt = 0;
		long mLastModified = 0;
		String mBody = null;
	}

	private static int SCACHE_MAX_SZ = 100;	// ~1 MiB of data
	@SuppressWarnings("serial")
	private static Map<URL,CacheEntry> sPageCache = new LinkedHashMap<URL,CacheEntry>(SCACHE_MAX_SZ, 0.75f, true) {
		protected boolean removeEldestEntry(Map.Entry<URL,CacheEntry> eldest) {
			return size() > SCACHE_MAX_SZ;
		}
	};

	private URLFetcher() {
		// Default constructor.
	}

	public static synchronized URLFetcher getInstance() {
		if (sInstance == null) sInstance = new URLFetcher();

		return sInstance;
	}

	public String fetchAsString(URL url) throws Exception {
		return fetchAsString(url, false, null);
	}

	public String fetchAsString(URL url, boolean forceCachedData) throws Exception {
		return fetchAsString(url, forceCachedData, null);
	}

	public String fetchAsString(URL url, boolean forceCachedData, Map<String,String> reqProps) throws Exception {
		boolean isCacheMiss = false;
		StringBuilder sb = new StringBuilder();
		CacheEntry cacheEntry = sPageCache.get(url);
		if (cacheEntry == null) {
			cacheEntry = new CacheEntry();
			isCacheMiss = true;
		} else {
			// Some sites don't set Last-Modified.  Some of them have data that rarely changes (Wowhead).
			// Add an override to force return cached data if it's reasonable to do so.
			if (forceCachedData && cacheEntry.mBody != null && System.currentTimeMillis() - cacheEntry.mBodyUpdatedAt < CACHE_TTL) {
				return cacheEntry.mBody;
			}
		}

		try {
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.setConnectTimeout(CONNECT_TIMEOUT);
			conn.setReadTimeout(READ_TIMEOUT);
			conn.setRequestProperty("Accept-Language", "en");	// English motherfucker, do you speak it?
			if (reqProps != null) {
				for (Map.Entry<String,String> entry : reqProps.entrySet()) {
					// Bolt on extra stuff to the request if specified.
					conn.setRequestProperty(entry.getKey(), entry.getValue());
				}
			}
			if (cacheEntry.mLastModified > 0) {
				conn.setRequestProperty("If-Modified-Since", sDateFormater.format(new Date(cacheEntry.mLastModified)));
			}

			InputStream is = conn.getInputStream();
			byte buf[] = new byte[BUFFER_SZ];
			int sz;

			do {
				sz = is.read(buf);
				if (sz != -1) {
					sb.append(new String(buf, 0, sz));
				}
			} while (sz != -1);

			if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
				if (!isCacheMiss) {
					return cacheEntry.mBody;
				}
				throw new RuntimeException("302 Error received with no cached data");
			}

			cacheEntry.mBodyUpdatedAt = System.currentTimeMillis();
			cacheEntry.mLastModified = conn.getHeaderFieldDate("Last-Modified", System.currentTimeMillis());
			cacheEntry.mBody = sb.toString();

			if (isCacheMiss) {
				sPageCache.put(url, cacheEntry);
			}
		} catch(Exception e) {
			throw e;
		}

		return cacheEntry.mBody;
	}
}
